
package org.spiderflow.core.expression.parsing.token;

import org.spiderflow.core.exception.StringLiteralException;
import org.spiderflow.core.exception.TemplateException;
import org.spiderflow.core.expression.ExpressionError;
import org.spiderflow.core.expression.parsing.CharacterStream;
import org.spiderflow.core.expression.parsing.Span;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;


/**
 * 分词器
 *
 * @author zsh
 */
public class Tokenizer {
    private static final String START_FLAG = "${";
    private static final String END_FLAG = "}";
    private static final String BYTE_FLAG = "b";
    private static final String BYTE_FLAG_CAPITAL = "B";
    private static final String SHORT_FLAG = "s";
    private static final String SHORT_FLAG_CAPITAL = "S";
    private static final String LONG_FLAG = "l";
    private static final String LONG_FLAG_CAPITAL = "L";
    private static final String FLOAT_FLAG = "f";
    private static final String FLOAT_FLAG_CAPITAL = "F";
    private static final String DOUBLE_FLAG = "d";
    private static final String DOUBLE_FLAG_CAPITAL = "D";

    /**
     * Participle the source into words with a {@link TokenType}. Text blocks not enclosed in {{ }} are returned as a single word
     * of type {@link TokenType#TEXT_BLOCK}. {{ and }} are not returned as individual words. See {@link TokenType} for the list of
     * tokens this tokenizer understands.
     */
    public List<Token> split(String source) {
        if (source.length() == 0) {
            return Collections.emptyList();
        }
        CharacterStream stream = new CharacterStream(source);
        stream.updateSpanStart();
        List<Token> tokens = splitStreamToList(stream);
        if (stream.isCurrentSpanNotEmpty()) {
            tokens.add(new Token(TokenType.TEXT_BLOCK, stream.buildCurrentSpan()));
        }
        return tokens;
    }


    private List<Token> splitStreamToList(CharacterStream stream) {
        List<Token> tokens = new ArrayList<>();
        while (stream.hasMore()) {
            if (stream.match(START_FLAG, false)) {
                if (stream.isCurrentSpanNotEmpty()) {
                    tokens.add(new Token(TokenType.TEXT_BLOCK, stream.buildCurrentSpan()));
                }
                stream.updateSpanStart();
                tokens.addAll(tokenizeStream(stream));
                stream.updateSpanStart();
            } else {
                stream.consume();
            }
        }
        return tokens;
    }

    private List<Token> tokenizeStream(CharacterStream stream) {
        TemplateException re;
        boolean isContinue = false;
        List<Token> tokens = new ArrayList<>();
        do {
            matchEndAndConsume(stream);
            try {
                tokens.addAll(tokenizeCodeSpan(stream.buildCurrentSpan()));
                isContinue = false;
                re = null;
            } catch (TemplateException e) {
                re = e;
                if (e.getCause() != null || stream.hasMore()) {
                    isContinue = true;
                }
            }
        } while (isContinue);
        if (re != null) {
            throw re;
        }
        return tokens;
    }

    private static List<Token> tokenizeCodeSpan(Span span) {
        String source = span.getSource();
        CharacterStream stream = new CharacterStream(source, span.getStart(), span.getEnd());
        // match opening tag and throw it away
        if (!stream.match(START_FLAG, true)) {
            throw ExpressionError.error("Expected ${", new Span(source, stream.getCurrentPosition(), stream.getCurrentPosition() + 1));
        }
        List<Token> tokens = buildTokenList(source, stream);
        // code spans must end with "\}"
        if (!stream.match(END_FLAG, true)) {
            throw ExpressionError.error("Expected }", new Span(source, stream.getCurrentPosition(), stream.getCurrentPosition() + 1));
        }
        return tokens;
    }

    private static List<Token> buildTokenList(String source, CharacterStream stream) {
        AtomicInteger leftCount = new AtomicInteger();
        AtomicInteger rightCount = new AtomicInteger();
        List<Token> tokens = new ArrayList<>();
        while (stream.hasMore()) {
            // skip whitespace
            stream.skipWhiteSpace();
            // Number literal, both integers and floats. Number literals may be suffixed by a type identifier.
            Token token;
            token = tryBuildToken(source, stream, leftCount, rightCount);
            if (token != null) {
                tokens.add(token);
            } else if (stream.match(END_FLAG, false)) {
                // match closing tag
                break;
            } else {
                throw ExpressionError.error("Unknown token", new Span(source, stream.getCurrentPosition(), stream.getCurrentPosition() + 1));
            }
        }
        return tokens;
    }

    private static Token tryBuildToken(String source, CharacterStream stream,
                                       AtomicInteger leftCount, AtomicInteger rightCount) {
        Token token;
        if (stream.matchDigit(false)) {
            token = buildDigitToken(stream);
        } else if (stream.match(TokenType.SINGLE_QUOTE.getLiteral(), true)) {
            token = buildStringToken(stream, TokenType.SINGLE_QUOTE);
        } else if (stream.match(TokenType.DOUBLE_QUOTE.getLiteral(), true)) {
            token = buildStringToken(stream, TokenType.DOUBLE_QUOTE);
        } else if (stream.matchIdentifierStart(true)) {
            token = buildIdentifierTypeToken(stream);
        } else {
            token = tryBuildOtherToken(source, stream, leftCount, rightCount);
        }
        return token;
    }

    /**
     * Identifier, keyword, boolean literal, or null literal
     *
     * @param stream CharacterStream
     * @return Identifier, keyword, boolean literal, or null literal
     */
    private static Token buildIdentifierTypeToken(CharacterStream stream) {
        // Identifier, keyword, boolean literal, or null literal
        stream.updateSpanStart();
        stream.matchIdentifierPartUntilFalse();
        Span identifierSpan = stream.buildCurrentSpanAndExpansion();
        String text = identifierSpan.getText();
        if ("true".equals(text) || "false".equals(text)) {
            return new Token(TokenType.BOOLEAN_LITERAL, identifierSpan);
        } else if ("null".equals(text)) {
            return new Token(TokenType.NULL_LITERAL, identifierSpan);
        } else {
            return new Token(TokenType.IDENTIFIER, identifierSpan);
        }
    }

    private static Token tryBuildOtherToken(String source, CharacterStream stream,
                                            AtomicInteger leftCount, AtomicInteger rightCount) {
        Token token = tryBuildSimpleToken(source, stream, leftCount);
        if (token != null) {
            return token;
        }
        if (leftCount.get() != rightCount.get() && stream.match(END_FLAG, true)) {
            rightCount.getAndIncrement();
            int currentPosition = stream.getCurrentPosition();
            return new Token(TokenType.RIGHT_CURLY, new Span(source, currentPosition - 1, currentPosition));
        }
        return null;
    }

    /**
     * Simple tokens
     *
     * @param source    source
     * @param stream    CharacterStream
     * @param leftCount '{' count
     * @return Simple token
     */
    private static Token tryBuildSimpleToken(String source, CharacterStream stream, AtomicInteger leftCount) {
        for (TokenType t : TokenType.getSortedValues()) {
            if (t.getLiteral() != null && stream.match(t.getLiteral(), true)) {
                if (t == TokenType.LEFT_CURLY) {
                    leftCount.getAndIncrement();
                }
                int currentPosition = stream.getCurrentPosition();
                int start = currentPosition - t.getLiteral().length();
                return new Token(t, new Span(source, start, currentPosition));
            }
        }
        return null;
    }

    /**
     * String literal
     *
     * @param stream CharacterStream
     * @param type   TokenType
     * @return String token
     */
    private static Token buildStringToken(CharacterStream stream, TokenType type) {
        stream.updateSpanStart();
        boolean matchedEndQuote = false;
        while (stream.hasMore()) {
            // Note: escape sequences like \n are parsed in StringLiteral
            if (stream.match("\\", true)) {
                stream.consume();
            }
            if (stream.match(type.getLiteral(), true)) {
                matchedEndQuote = true;
                break;
            }
            stream.consume();
        }
        if (!matchedEndQuote) {
            throw ExpressionError.error("字符串没有结束符\"", stream.buildCurrentSpan(), new StringLiteralException());
        }
        Span stringSpan = stream.buildCurrentSpan();
        stringSpan = new Span(stringSpan.getSource(), stringSpan.getStart() - 1, stringSpan.getEnd());
        return new Token(TokenType.STRING_LITERAL, stringSpan);
    }

    /**
     * Digit literal
     *
     * @param stream CharacterStream
     * @return Digit token
     */
    private static Token buildDigitToken(CharacterStream stream) {
        stream.updateSpanStart();
        stream.matchNextNan(true);
        TokenType type = getNumberTokenType(stream);
        Span numberSpan = stream.buildCurrentSpan();
        return new Token(type, numberSpan);
    }


    private static void matchEndAndConsume(CharacterStream stream) {
        while (!stream.match(END_FLAG, true)) {
            if (!stream.hasMore()) {
                throw ExpressionError.error("Did not find closing }.", stream.buildCurrentSpan());
            }
            stream.consume();
        }
    }

    private static TokenType getNumberTokenType(CharacterStream stream) {
        TokenType type = initNumberType(stream);
        if (stream.match(BYTE_FLAG, true) || stream.match(BYTE_FLAG_CAPITAL, true)) {
            checkType(stream, type, "Byte");
            type = TokenType.BYTE_LITERAL;
        } else if (stream.match(SHORT_FLAG, true) || stream.match(SHORT_FLAG_CAPITAL, true)) {
            checkType(stream, type, "Short");
            type = TokenType.SHORT_LITERAL;
        } else if (stream.match(LONG_FLAG, true) || stream.match(LONG_FLAG_CAPITAL, true)) {
            checkType(stream, type, "Long");
            type = TokenType.LONG_LITERAL;
        } else if (stream.match(FLOAT_FLAG, true) || stream.match(FLOAT_FLAG_CAPITAL, true)) {
            type = TokenType.FLOAT_LITERAL;
        } else if (stream.match(DOUBLE_FLAG, true) || stream.match(DOUBLE_FLAG_CAPITAL, true)) {
            type = TokenType.DOUBLE_LITERAL;
        }
        return type;
    }

    private static void checkType(CharacterStream stream, TokenType type, String errorType) {
        if (type == TokenType.DOUBLE_LITERAL) {
            String message = errorType + " literal can not have a decimal point.";
            throw ExpressionError.error(message, stream.buildCurrentSpan());
        }
    }

    private static TokenType initNumberType(CharacterStream stream) {
        TokenType type = TokenType.INTEGER_LITERAL;
        if (stream.match(TokenType.PERIOD.getLiteral(), true)) {
            type = TokenType.DOUBLE_LITERAL;
            stream.matchNextNan(true);
        }
        return type;
    }
}
